/*
Copyright Contributors to the libdnf project.

This file is part of libdnf: https://github.com/rpm-software-management/libdnf/

Libdnf is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.

Libdnf is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with libdnf.  If not, see <https://www.gnu.org/licenses/>.
*/

#include "setopt.hpp"

#include "shared.hpp"

#include <libdnf5/common/sack/match_string.hpp>
#include <libdnf5/conf/const.hpp>
#include <libdnf5/utils/bgettext/bgettext-mark-domain.h>

#include <filesystem>

namespace dnf5 {

using namespace libdnf5;

namespace {

constexpr std::string_view REPOS_OVERRIDE_CFG_HEADER =
    "# Generated by dnf5 config-manager.\n# Do not modify this file manually, use dnf5 config-manager instead.\n";


void modify_config(
    ConfigParser & parser, const std::string & section_id, const std::map<std::string, std::string> & opts) {
    if (!parser.has_section(section_id)) {
        parser.add_section(section_id);
    }
    for (const auto & [key, value] : opts) {
        parser.set_value(section_id, key, value, "");
    }
}


std::set<std::string> filter_repo_ids(const std::string & pattern, const std::set<std::string> & repo_ids) {
    std::set<std::string> matched_repo_ids;
    for (const auto & repo_id : repo_ids) {
        if (sack::match_string(repo_id, sack::QueryCmp::GLOB, pattern)) {
            matched_repo_ids.insert(repo_id);
        }
    }
    return matched_repo_ids;
}

}  // namespace


void ConfigManagerSetOptCommand::set_argument_parser() {
    auto & ctx = get_context();
    auto & parser = ctx.get_argument_parser();

    auto & cmd = *get_argument_parser_command();
    cmd.set_description("Set configuration and repositories options");

    auto opts_vals =
        parser.add_new_positional_arg("optvals", cli::ArgumentParser::PositionalArg::AT_LEAST_ONE, nullptr, nullptr);
    opts_vals->set_description("List of options with values. Format: \"[REPO_ID.]option=value\"");
    opts_vals->set_parse_hook_func([this](
                                       [[maybe_unused]] cli::ArgumentParser::PositionalArg * arg,
                                       int argc,
                                       const char * const argv[]) {
        for (int i = 0; i < argc; ++i) {
            auto value = argv[i];
            auto val = strchr(value + 1, '=');
            if (!val) {
                throw cli::ArgumentParserError(
                    M_("{}: Badly formatted argument value \"{}\""), std::string{"optval"}, std::string{value});
            }
            std::string key{value, val};
            std::string key_value{val + 1};
            auto dot_pos = key.rfind('.');
            if (dot_pos != std::string::npos) {
                if (dot_pos == key.size() - 1) {
                    throw cli::ArgumentParserError(
                        M_("{}: Badly formatted argument value: Last key character cannot be '.': {}"),
                        std::string{"optval"},
                        std::string{value});
                }

                // Save the repository option for later processing (solving glob pattern, writing to file).
                auto repo_id = key.substr(0, dot_pos);
                if (repo_id.empty()) {
                    throw cli::ArgumentParserError(
                        M_("{}: Empty repository id is not allowed: {}"), std::string{"optval"}, std::string{value});
                }
                auto repo_key = key.substr(dot_pos + 1);

                // Test if the repository option is known and can be set.
                try {
                    tmp_repo_conf.opt_binds().at(repo_key).new_string(Option::Priority::COMMANDLINE, key_value);
                } catch (const Error & ex) {
                    throw ConfigManagerError(
                        M_("Cannot set repository option \"{}\": {}"), std::string{value}, std::string{ex.what()});
                }

                const auto [it, inserted] = in_repos_setopts[repo_id].insert({repo_key, key_value});
                if (!inserted) {
                    if (it->second != key_value) {
                        throw ConfigManagerError(
                            M_("Sets the \"{}\" option of the repository \"{}\" again with a different value: \"{}\" "
                               "!= \"{}\""),
                            repo_key,
                            repo_id,
                            it->second,
                            key_value);
                    }
                }
            } else {
                // Test if the global option is known and can be set.
                try {
                    tmp_config.opt_binds().at(key).new_string(Option::Priority::COMMANDLINE, key_value);
                } catch (const Error & ex) {
                    throw ConfigManagerError(
                        M_("Cannot set option: \"{}\": {}"), std::string{value}, std::string(ex.what()));
                }

                // Save the global option for later writing to a file.
                const auto [it, inserted] = main_setopts.insert({key, key_value});
                if (!inserted) {
                    if (it->second != key_value) {
                        throw ConfigManagerError(
                            M_("Sets the \"{}\" option again with a different value: \"{}\" != \"{}\""),
                            key,
                            it->second,
                            key_value);
                    }
                }
            }
        }
        return true;
    });
    cmd.register_positional_arg(opts_vals);

    auto create_missing_dirs_opt = parser.add_new_named_arg("create-missing-dir");
    create_missing_dirs_opt->set_long_name("create-missing-dir");
    create_missing_dirs_opt->set_description("Allow to create missing directories");
    create_missing_dirs_opt->set_has_value(false);
    create_missing_dirs_opt->set_parse_hook_func([this](cli::ArgumentParser::NamedArg *, const char *, const char *) {
        create_missing_dirs = true;
        return true;
    });
    cmd.register_named_arg(create_missing_dirs_opt);
}


void ConfigManagerSetOptCommand::configure() {
    auto & ctx = get_context();
    const auto & config = ctx.get_base().get_config();

    auto repo_ids = load_existing_repo_ids();
    for (auto & [in_repo_id, repo_setopts] : in_repos_setopts) {
        auto filtered_repo_ids = filter_repo_ids(in_repo_id, repo_ids);
        if (filtered_repo_ids.empty()) {
            throw ConfigManagerError(M_("No matching repository to modify: {}"), in_repo_id);
        }
        for (const auto & repo_id : filtered_repo_ids) {
            for (const auto & [key, value] : repo_setopts) {
                // Save the repository option for later writing to a file.
                const auto [it, inserted] = matching_repos_setopts[repo_id].insert({key, value});
                if (!inserted) {
                    if (it->second != value) {
                        throw ConfigManagerError(
                            M_("Sets the \"{}\" option of the repository \"{}\" again with a different value: \"{}\" "
                               "!= \"{}\""),
                            key,
                            repo_id,
                            it->second,
                            value);
                    }
                }
            }
        }
    }

    // Write new and modify existing options in the main configuration file.
    if (!main_setopts.empty()) {
        ConfigParser parser;

        const auto & cfg_filepath = get_config_file_path(config);
        resolve_missing_dir(cfg_filepath.parent_path(), create_missing_dirs);

        const bool exists = std::filesystem::exists(cfg_filepath);
        if (exists) {
            parser.read(cfg_filepath);
        }

        modify_config(parser, "main", main_setopts);
        parser.write(cfg_filepath, false);
        if (!exists) {
            set_file_permissions(cfg_filepath);
        }
    }

    // Write new and modify existing options in the repositories overrides configuration file.
    if (!matching_repos_setopts.empty()) {
        ConfigParser parser;

        resolve_missing_dir(get_repos_config_override_dir_path(config), create_missing_dirs);

        auto repos_override_file_path = get_config_manager_repos_override_file_path(config);

        const bool exists = std::filesystem::exists(repos_override_file_path);
        if (exists) {
            parser.read(repos_override_file_path);
        }

        parser.get_header() = REPOS_OVERRIDE_CFG_HEADER;

        for (const auto & [repo_id, repo_opts] : matching_repos_setopts) {
            modify_config(parser, repo_id, repo_opts);
        }

        parser.write(repos_override_file_path, false);
        if (!exists) {
            set_file_permissions(repos_override_file_path);
        }
    }
}


std::set<std::string> ConfigManagerSetOptCommand::load_existing_repo_ids() const {
    auto & ctx = get_context();
    auto & base = ctx.get_base();
    auto logger = base.get_logger();

    std::set<std::string> repo_ids;

    // The repository can also be defined in the main configuration file.
    if (const auto & conf_path = get_config_file_path(base.get_config()); std::filesystem::exists(conf_path)) {
        ConfigParser parser;
        parser.read(conf_path);
        for (const auto & [section, opts] : parser.get_data()) {
            if (section == "main") {
                continue;
            }
            repo_ids.insert(section);
        }
    }

    const auto & repo_dirs = base.get_config().get_reposdir_option().get_value();
    for (const std::filesystem::path dir : repo_dirs) {
        if (std::filesystem::exists(dir)) {
            std::error_code ec;
            std::filesystem::directory_iterator di(dir, ec);
            if (ec) {
                write_warning(
                    *logger, M_("Cannot read repositories from directory \"{}\": {}"), dir.string(), ec.message());
                continue;
            }
            for (auto & dentry : di) {
                const auto & path = dentry.path();
                if (path.extension() == ".repo") {
                    ConfigParser parser;
                    parser.read(path);
                    for (const auto & [repo_id, opts] : parser.get_data()) {
                        repo_ids.insert(repo_id);
                    }
                }
            }
        }
    }

    return repo_ids;
}

}  // namespace dnf5
